﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace DynamicProperties
{
	/// <summary>
	/// Generic definition of dynamic property.
	/// </summary>
	public sealed class DynamicProperty<TOwner, TProperty> : DynamicProperty where TOwner : class
	{
		private Type targetType, propertyType;
		
		private bool defaultValueIsDefined;
		private TProperty defaultValue;
		private Func<TProperty> defaultValueFactory;
		
		/// <summary>
		/// Creates <see cref="dynamic property"/>.
		/// </summary>
		/// <param name="description">Description of dynamic property. Can be null or empty.</param>
		/// <param name="defaultValue">Default value of property. Can be null.</param>
		/// <param name="changePropertyCallback">Callback that is invoked when value is changed. Can be null.</param>
		public DynamicProperty(string description, TProperty defaultValue, DynamicPropertyChangedCallback<TOwner, TProperty> changePropertyCallback)
			: base(description)
		{
			Init();
			InitDefaultValue(defaultValue);
			InitCallback(changePropertyCallback);
		}

		/// <summary>
		/// Creates <see cref="dynamic property"/>.
		/// </summary>
		/// <param name="description">Description of dynamic property. Can be null or empty.</param>
		/// <param name="defaultValueFactory">Default value factory for property. Can be null.</param>
		/// <param name="changePropertyCallback">Callback that is invoked when value is changed. Can be null.</param>
		public DynamicProperty(string description, Func<TProperty> defaultValueFactory, DynamicPropertyChangedCallback<TOwner, TProperty> changePropertyCallback)
			: base(description)
		{
			Init();
			InitDefaultValue(defaultValueFactory);
			InitCallback(changePropertyCallback);
		}

		/// <summary>
		/// Creates <see cref="dynamic property"/>.
		/// </summary>
		/// <param name="description">Description of dynamic property. Can be null or empty.</param>
		/// <param name="changePropertyCallback">Callback that is invoked when value is changed. Can be null.</param>
		public DynamicProperty(string description, DynamicPropertyChangedCallback<TOwner, TProperty> changePropertyCallback)
			: base(description)
		{
			Init();
			InitCallback(changePropertyCallback);
		}

		/// <summary>
		/// Creates <see cref="dynamic property"/>.
		/// </summary>
		/// <param name="description">Description of dynamic property. Can be null or empty.</param>
		/// <param name="defaultValue">Default value of property. Can be null.</param>
		public DynamicProperty(string description, TProperty defaultValue)
			: base(description)
		{
			Init();
			InitDefaultValue(defaultValue);
		}

		/// <summary>
		/// Creates <see cref="dynamic property"/>.
		/// </summary>
		/// <param name="description">Description of dynamic property. Can be null or empty.</param>
		/// <param name="defaultValueFactory">Default value factory for property. Can be null.</param>
		public DynamicProperty(string description, Func<TProperty> defaultValueFactory)
			: base(description)
		{
			Init();
			InitDefaultValue(defaultValueFactory);
		}

		/// <summary>
		/// Creates <see cref="dynamic property"/>.
		/// </summary>
		/// <param name="description">Description of dynamic property. Can be null or empty.</param>
		public DynamicProperty(string description)
			: base(description)
		{
			Init();
		}

		/// <summary>
		/// Gets the type that dynamic property is defined for.
		/// </summary>
		public override Type OwnerType
		{
			get { return targetType; }
		}

		/// <summary>
		/// Gets the type that dynamic property uses for its value.
		/// </summary>
		public override Type PropertyType
		{
			get { return propertyType; }
		}

		private void Init()
		{
			this.targetType = typeof(TOwner);
			this.propertyType = typeof(TProperty);
		}

		private void InitDefaultValue(TProperty defaultValue)
		{
			this.defaultValueIsDefined = true;
			this.defaultValue = defaultValue;
		}

		private void InitDefaultValue(Func<TProperty> defaultValueFactory)
		{
			this.defaultValueFactory = defaultValueFactory;
		}

		private void InitCallback(DynamicPropertyChangedCallback<TOwner, TProperty> changePropertyCallback)
		{
			if (changePropertyCallback != null)
				this.OnChanged += changePropertyCallback;
		}

		internal protected override object GetDefaultValue()
		{
			if (defaultValueFactory != null)
			{
				// delegate with func to create new instance
				var result = defaultValueFactory.Invoke();
				return result;
			}
			else if (defaultValueIsDefined)
			{
				// static default value
				return defaultValue;
			}
			else
			{
				// default value for type
				return default(TProperty);
			}
		}

		protected internal override void OnPropertyUpdated(object target, object oldValue, object newValue)
		{
			// skip this if this is internal event subscription property - we don't need to track its changes
			if (object.ReferenceEquals(this, EventSubscribers.EventSubscribersProperty))
				return;

			// get local event subscribers (on specific target object)
			bool eventLocalHasSubscribers = false;
			EventSubscribers eventLocalHolder;
			if(EventSubscribers.EventSubscribersProperty.TryGet(target, out eventLocalHolder))
			{
				// check if there is any event subscribers
				eventLocalHasSubscribers = eventLocalHolder.GetHasAnySubscribers(this);
			}

			// get global event subscribers (defined on property definition)
			var eventGlobal = this.OnChanged;

			// no subscribers attached
			if (eventGlobal == null && eventLocalHasSubscribers == false)
				return;

			// cast types
			TOwner targetTyped = (TOwner)target;
			TProperty oldValueTyped = (TProperty)oldValue;
			TProperty newValueTyped = (TProperty)newValue;

			// create args
			var args = new DynamicPropertyChangedParams<TOwner, TProperty>(this, oldValueTyped, newValueTyped);
				
			// invoke local event on current target object
			if (eventLocalHasSubscribers)
				eventLocalHolder.InvokeEvent(targetTyped, args);

			// invoke global event
			if (eventGlobal != null)
				eventGlobal.Invoke(targetTyped, args);
		}

		/// <summary>
		/// Event that is invoked when property is changed on any object.
		/// </summary>
		public event DynamicPropertyChangedCallback<TOwner, TProperty> OnChanged;


		/// <summary>
		/// Sets this dynamic property to given object to specific value.
		/// </summary>
		/// <param name="target">Target object.</param>
		/// <param name="value">Value to set.</param>
		/// <param name="extender">Extender used to store values. If null, global extender is used.</param>
		public void Set(TOwner target, TProperty value, Extender extender = null)
		{
			// default extender
			if (extender == null)
				extender = Extender.Global;

			extender.SetProperty(target, this, value);
		}

		/// <summary>
		/// Gets value of this dynamic property from given object.
		/// </summary>
		/// <param name="target">Target object.</param>
		/// <param name="extender">Extender used to store values. If null, global extender is used.</param>
		public TProperty Get(TOwner target, Extender extender = null)
		{
			// default extender
			if (extender == null)
				extender = Extender.Global;

			TProperty result = extender.GetProperty(target, this);
			return result;
		}

		/// <summary>
		/// Gets value of this dynamic property from given object.
		/// </summary>
		/// <param name="target">Target object.</param>
		/// <param name="result">Result object.</param>
		/// <param name="extender">Extender used to store values. If null, global extender is used.</param>
		/// <returns>Returns true if value was found on given object. False is returned if value is null or not defined yet.</returns>
		public bool TryGet(TOwner target, out TProperty result, Extender extender = null)
		{
			// default extender
			if (extender == null)
				extender = Extender.Global;

			bool retVal = extender.TryGetProperty(target, this, out result);
			return retVal;
		}
	}

	/// <summary>
	/// Represents the callback that is invoked when dynamic property is changed.
	/// </summary>
	/// <param name="target">Target object that is changing.</param>
	/// <param name="oldValue">Old property value.</param>
	/// <param name="newValue">New property value.</param>
	/// <param name="property">Changing property.</param>
	public delegate void DynamicPropertyChangedCallback<TOwner, TProperty>(TOwner target, DynamicPropertyChangedParams<TOwner, TProperty> args) where TOwner : class;
}
